import { MemoryStorageFile } from '@blazity/nest-file-fastify';
import { ConstantEnum } from '@libs/common/enums';
import { BaseRepository, indexPaginateBuilder } from '@libs/common/repository';
import { importJson, readExcel } from '@libs/common/utils';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { ArrayToObject } from '@shencom/utils-array';
import { Dayjs, FormatDate, FormatDateTime } from '@shencom/utils-date';
import { plainToInstance } from 'class-transformer';
import { Between, Brackets, DataSource, FindManyOptions } from 'typeorm';

import { DepartmentConfigService } from '@/dingding/department-config/department-config.service';
import { PointsWait } from '@/market/points-wait/entities/points-wait.entity';
import { PointsWaitService } from '@/market/points-wait/points-wait.service';

import { DingdingService } from '../../dingding/dingding.service';
import { MemberService } from '../../dingding/member/member.service';
import {
  AttendanceStatisticIndexDto,
  ColumnAttendanceStatisticDto,
  CreateAttendanceStatisticDto,
  FindAttendanceStatisticDto,
  ImportAttendanceStatisticDto,
  StatisticAttendanceStatisticDto,
  StatisticAttendanceWorkHoursDto,
  SyncAttendanceStatisticDto,
} from './dto/attendance-statistic.dto';
import {
  AttendanceStatistic,
  ColumnEnum,
} from './entities/attendance-statistic.entity';
import { attendanceStatisticsExcelToJson } from './utils';

interface DingAttendance {
  name: string;
  group: string;
  department?: string;
  dingId: string;
  attendanceClass?: string;
  workDate?: string;
  planDetail?: string;
  shouldAttendanceDays?: string;
  makingUpLackTimes?: string;
  onDutyUserCheckTime?: string;
  onDutyUserCheckResult?: string;
  offDutyUserCheckTime?: any;
  offDutyUserCheckResult?: string;
  attendanceDays?: string;
  attendanceRestDays?: any;
  attendanceWorkTime?: string;
  lateTimes?: string;
  lateMinute?: any;
  seriousLateTimes?: string;
  seriousLateMinute?: any;
  leaveEarlyTimes?: string;
  leaveEarlyMinute?: string;
  onWorkLackCardTimes?: string;
  offWorkLackCardTimes?: string;
  absenteeismDays?: string;
}

@Injectable()
export class AttendanceStatisticsService extends BaseRepository {
  constructor(
    @Inject(ConstantEnum.DB_DATA_SOURCE_MANAGER) dataSource: DataSource,
  ) {
    super(dataSource);
  }

  protected repositoryManager = () => this.getRepository(AttendanceStatistic);

  protected repository = this.getRepository(AttendanceStatistic);

  @Inject(MemberService)
  private memberService: MemberService;

  @Inject(DingdingService)
  private dingdingService: DingdingService;

  @Inject(DepartmentConfigService)
  private departmentConfigService: DepartmentConfigService;

  @Inject(PointsWaitService)
  private pointsWaitService: PointsWaitService;

  async index(dto: AttendanceStatisticIndexDto) {
    const res = await indexPaginateBuilder({
      dto,
      repository: this.repository,
    });
    return res;
  }

  async find(dto?: FindManyOptions<AttendanceStatistic>['where']) {
    return this.repository.find({ where: dto || {} });
  }

  async create(
    dto: CreateAttendanceStatisticDto | CreateAttendanceStatisticDto[],
  ) {
    const dtos = Array.isArray(dto) ? dto : [dto];
    return this.repositoryManager().save(dtos, { chunk: 500 });
  }

  async getColumnVal(dto: ColumnAttendanceStatisticDto) {
    const res = await this.dingdingService.getColumnVal({
      column_id_list: Object.values(ColumnEnum)
        .map((v) => v.id)
        .join(','),
      userid: dto.dingId,
      from_date: dto.fromDate,
      to_date: dto.toDate,
    });

    const { column_vals } = res;
    const workDate = column_vals[0].column_vals.map((v) => ({
      workDate: v.date,
    }));

    const entities: Record<string, any>[] = workDate;
    column_vals.forEach((item) => {
      const columns = Object.keys(ColumnEnum).map((k) => ({
        ...ColumnEnum[k],
        prop: k,
      })) as { id: number; prop: string; name: string }[];

      const column = columns.find((k) => k.id === item.column_vo.id);

      const prop = column?.prop;

      if (!prop) return;

      item.column_vals.forEach((v, i) => {
        entities[i] = { ...entities[i], dingId: dto.dingId, [prop]: v.value };
      });
    });

    return entities;
  }

  private statistics(dto: StatisticAttendanceStatisticDto) {
    const queryBuilder = this.repository.createQueryBuilder(
      'crm_attendance_statistic',
    );
    queryBuilder.where('crm_attendance_statistic.dingId = :dingId', {
      dingId: dto.dingId,
    });
    if (dto.start && dto.end) {
      if (!Dayjs(dto.start).isBefore(Dayjs(dto.end))) {
        throw new BadRequestException('开始时间大于结束时间');
      }
      queryBuilder.andWhere('crm_attendance_statistic.workDate >= :start', {
        start: dto.start,
      });
      queryBuilder.andWhere('crm_attendance_statistic.workDate <= :end', {
        end: dto.end,
      });
    }
    return queryBuilder;
  }

  async userOverview(dto: StatisticAttendanceStatisticDto) {
    const queryBuilder = this.statistics(dto);

    const dataColumn = {
      shouldAttendanceDays: {
        count: 0,
        label: ColumnEnum.shouldAttendanceDays.name,
      },
      attendanceRestDays: {
        count: 0,
        label: ColumnEnum.attendanceRestDays.name,
      },
      attendanceDays: { count: 0, label: ColumnEnum.attendanceDays.name },
      absenteeismDays: { count: 0, label: ColumnEnum.absenteeismDays.name },
      leaveEarlyMinute: {
        count: 0,
        label: ColumnEnum.leaveEarlyMinute.name,
      },
      attendanceWorkTime: {
        count: 0,
        label: ColumnEnum.attendanceWorkTime.name,
      },
      lateMinute: { count: 0, label: ColumnEnum.lateMinute.name },
      lateTimes: { count: 0, label: ColumnEnum.lateTimes.name },
      leaveEarlyTimes: {
        count: 0,
        label: ColumnEnum.leaveEarlyTimes.name,
      },
      makingUpLackTimes: {
        count: 0,
        label: ColumnEnum.makingUpLackTimes.name,
      },
      seriousLateTimes: {
        count: 0,
        label: ColumnEnum.seriousLateTimes.name,
      },
      seriousLateMinute: {
        count: 0,
        label: ColumnEnum.seriousLateMinute.name,
      },
    };

    const extraColumn = {
      overtimeDays: { count: 0, label: '休息日加班天数' },
      workDate: { count: 0, label: '条数' },
      outWorkDays: { count: 0, label: '外出天数' },
      businessTripDays: { count: 0, label: '出差天数' },
    };

    const columns = { ...dataColumn, ...extraColumn };

    const raw = await queryBuilder.getMany();

    raw.forEach((item) => {
      if (item.workDate) columns.workDate.count++;
      if (item.shouldAttendanceDays) {
        columns.shouldAttendanceDays.count += item.shouldAttendanceDays;
      }
      if (item.attendanceRestDays) {
        columns.attendanceRestDays.count += item.attendanceRestDays;
      }
      if (item.attendanceDays) {
        columns.attendanceDays.count++;
      }
      if (item.absenteeismDays) {
        columns.absenteeismDays.count += item.absenteeismDays;
      }
      if (item.attendanceRestDays && item.attendanceDays) {
        columns.overtimeDays.count++;
      }
      if (item.leaveEarlyMinute) {
        columns.leaveEarlyMinute.count += item.leaveEarlyMinute;
      }
      if (item.attendanceWorkTime) {
        columns.attendanceWorkTime.count += item.attendanceWorkTime;
      }
      if (item.lateMinute) {
        columns.lateMinute.count += item.lateMinute;
      }
      if (item.lateTimes) {
        columns.lateTimes.count += item.lateTimes;
      }
      if (item.leaveEarlyTimes) {
        columns.leaveEarlyTimes.count += item.leaveEarlyTimes;
      }
      if (item.makingUpLackTimes) {
        columns.makingUpLackTimes.count += item.makingUpLackTimes;
      }
      if (item.seriousLateTimes) {
        columns.seriousLateTimes.count += item.seriousLateTimes;
      }
      if (item.seriousLateMinute) {
        columns.seriousLateMinute.count += item.seriousLateMinute;
      }
      if (
        item.onDutyUserCheckResult === '外出' &&
        item.offDutyUserCheckResult === '外出'
      ) {
        columns.outWorkDays.count++;
      }
      if (
        item.onDutyUserCheckResult === '出差' &&
        item.offDutyUserCheckResult === '出差'
      ) {
        columns.businessTripDays.count++;
      }
    });

    return columns;
  }

  async userWorkHours(dto: StatisticAttendanceWorkHoursDto) {
    // 如何有值，就不需要查询
    if (dto.type && dto.type !== 'day') {
      delete dto.start;
      delete dto.end;
    }

    const queryBuilder = this.statistics(dto);
    queryBuilder.andWhere(
      new Brackets((qb) => {
        qb.where(
          'crm_attendance_statistic.absenteeismDays IS NOT NULL',
        ).orWhere('crm_attendance_statistic.attendanceWorkTime IS NOT NULL');
      }),
    );
    queryBuilder.select([
      'crm_attendance_statistic.workDate',
      'crm_attendance_statistic.attendanceWorkTime',
      'crm_attendance_statistic.shouldAttendanceDays',
      'crm_attendance_statistic.attendanceRestDays',
    ]);

    const raw = await queryBuilder.getMany();

    if (!dto.type || dto.type === 'day') {
      return raw.map((v) => ({
        ...v,
        workDate: Dayjs(v.workDate).format('YYYY-MM-DD'),
      }));
    }
    if (dto.type === 'year' || dto.type === 'quarter' || dto.type === 'month') {
      const format = { year: 'YYYY', month: 'YYYY-MM', quarter: 'Q' } as const;

      const data = raw.reduce((prev, curr) => {
        const day = Dayjs(curr.workDate);
        const date = day.format(format[dto.type || 'day']);
        if (!prev[date]) {
          prev[date] = {
            attendanceWorkTime: 0,
            shouldAttendanceDays: 0,
            workDate:
              dto.type === 'quarter'
                ? `${day.format('YYYY')}/${date}季度`
                : date,
          };
        }

        prev[date].attendanceWorkTime +=
          Math.floor((curr.attendanceWorkTime / 60) * 100) / 100;

        prev[date].shouldAttendanceDays += curr.shouldAttendanceDays;
        return prev;
      }, {});

      return Object.values(data);
    }
  }

  // 每周 1 ,0 点执行一次, 取前一周之前的考勤
  // @Cron('0 0 0 * * 1')
  // 目前使用的为 import 导入 excel 的方式，不需要定时任务
  async timeGet() {
    const toDate = FormatDateTime(
      Dayjs().subtract(7, 'day').startOf('day').valueOf(),
    );
    // 容错 2 天
    const fromDate = FormatDateTime(
      Dayjs(toDate)
        .subtract(7 + 2, 'day')
        .endOf('day')
        .valueOf(),
    );

    await this.syncAttendanceLog({ fromDate: fromDate, toDate: toDate });

    await this.createPointsLog({ fromDate: fromDate, toDate: toDate });
  }

  /** 同步考勤统计记录 */
  async syncAttendanceLog(dto: SyncAttendanceStatisticDto) {
    // 获取钉钉用户列表
    const ding = await this.memberService.find({ status: 1 });

    const errIds: string[] = [];

    // 开始时间和结束时间必须在 15 天以内
    if (
      Dayjs(dto.fromDate).isBefore(Dayjs(dto.toDate).subtract(15, 'day'), 'day')
    ) {
      throw new BadRequestException('开始时间和结束时间必须在 15 天以内');
    }

    for (const item of ding) {
      try {
        const entities = await this.getColumnVal({
          ...dto,
          dingId: item.dingUserId,
        });

        const entity = plainToInstance(
          AttendanceStatistic,
          entities.map((v) => ({
            ...v,
            dingId: item.dingUserId,
            userName: item.name,
            userId: item.userId,
            department: item.deptNameList,
          })),
        );

        await this.create(entity);
      } catch (error) {
        errIds.push(item.id);
      }
    }

    if (errIds.length) {
      throw new BadRequestException(
        new Error('同步失败的列表：' + errIds.join(',')),
      );
    }

    return ding.length;
  }

  async updateData() {
    let data = (await importJson('/data/statistics')) as any[];

    data = data.filter((v) => v.makingUpLackTimes);

    const instances: {
      criteria: { dingId: string; workDate: Date };
      partialEntity: { makingUpLackTimes?: number };
    }[] = [];

    for (const item of data) {
      const instance = {
        // 通过联合索引找数据
        criteria: {
          dingId: item.dingId,
          workDate: Dayjs(+item.workDate).toDate(),
        },
        partialEntity: {
          // 要更新的值
          // makingUpLackTimes: item.makingUpLackTimes,
        },
      };
      instances.push(instance);
    }

    for (const instance of instances) {
      await this.repository.update(instance.criteria, instance.partialEntity);
    }

    return data.length;
  }

  async import(dto: ImportAttendanceStatisticDto, file: MemoryStorageFile) {
    const workbook = await readExcel(file.buffer);

    const data = attendanceStatisticsExcelToJson(workbook);

    if (!data.length) return;

    const member = await this.memberService.find();

    const instances = plainToInstance(
      AttendanceStatistic,
      data
        .map((item) => {
          const user = member.find((e) => e.dingUserId === item.dingId);

          if (!user) return;
          return {
            ...item,
            userName: user?.name || item.name,
            userId: user?.userId,
            department: user?.deptNameList || item.department,
          };
        })
        .filter(Boolean),
    );

    await this.batchCreate(instances);

    const fromDate = FormatDate(Dayjs(dto.date).startOf('month').valueOf());
    const toDate = FormatDate(Dayjs(dto.date).endOf('month').valueOf());

    await this.createPointsLog({ fromDate, toDate });

    return instances.length;
  }

  // 考勤记录转待审核积分记录
  async createPointsLog(dto: SyncAttendanceStatisticDto) {
    // 获取考勤记录
    const statistic = await this.repository.find({
      where: { workDate: Between(dto.fromDate, dto.toDate) },
    });

    if (!statistic.length) return;

    // 获取在职员工列表
    const member = await this.memberService.find({ status: 1 });

    const pointsRatio = await this.departmentConfigService.find({ status: 1 });
    const pointsRatioObj = ArrayToObject(pointsRatio, 'id');

    const list: PointsWait[] = [];
    statistic.forEach((item) => {
      const user = member.find((m) => m.userId === item.userId);
      if (!user) return;

      const dept = user.deptIdList.split(',').at(0);
      const ratio = pointsRatioObj?.[dept!]?.[0]?.ratio || 0.5;

      const vo = <PointsWait>{};
      vo.userId = user.userId;
      vo.userName = user.name;
      vo.departmentId = dept || '';
      vo.departmentName =
        item.department || user.deptNameList.split(',').at(0) || '';
      vo.points = 0;
      vo.dingId = user.dingUserId;

      let workPoints = 0;
      let extraWorkPoints = 0;
      let holidayWorkPoints = 0;

      // 上班积分 = (出勤天数 ｜｜ 出差 ｜｜ 外出) * 80
      if (
        (item.onDutyUserCheckResult === '外出' &&
          item.offDutyUserCheckResult === '外出') ||
        (item.onDutyUserCheckResult === '出差' &&
          item.offDutyUserCheckResult === '出差') ||
        item.attendanceDays
      ) {
        workPoints = 1 * 80;
      }

      // 额外加班积分 = 出勤 && (工时 - ((7 + 2) * 60 )) * 1 * 比例
      // 7 个小时工作，延迟 2 小时后计算，每分钟给 1 积分, 加兑换比例
      if (item.attendanceWorkTime) {
        const attendanceWorkTime = (item.attendanceWorkTime - (7 + 2) * 60) * 1;
        extraWorkPoints = Math.round(
          attendanceWorkTime > 0 ? attendanceWorkTime * ratio : 0,
        );
      }
      if (item.attendanceRestDays && item.attendanceWorkTime) {
        // 非工作日上班积分 = Math.round(工时 / 60) *  (20 * 比例)
        // 非工作日加班，每小时给 20 积分, 加兑换比例
        holidayWorkPoints =
          Math.round(item.attendanceWorkTime / 60) * (20 * ratio);
      }
      vo.points = Math.round(
        (workPoints + extraWorkPoints + holidayWorkPoints) * ratio,
      );

      if (vo.points <= 0) return;

      vo.remark = `上班：${workPoints}、额外上班：${extraWorkPoints}、非工作日上班：${holidayWorkPoints}、考勤日期：${FormatDate(
        item.workDate,
      )}`;

      list.push(vo);
    });

    await this.pointsWaitService.create(list);
  }

  private async batchCreate(dto: CreateAttendanceStatisticDto[]) {
    if (!dto.length) return;

    const conditions = dto.map((entity) => ({
      workDate: entity.workDate,
      dingId: entity.dingId,
    }));

    // 使用组合键进行查询
    const existingRecords = await this.repository.find({
      where: conditions,
    });

    // 创建一个 Set 用于存储已存在记录的键
    const existingKeys = new Set(
      existingRecords.map((record) => `${record.workDate}-${record.dingId}`),
    );

    // 过滤掉已存在的记录
    const newRecords = dto.filter(
      (entity) => !existingKeys.has(`${entity.workDate}-${entity.dingId}`),
    );

    if (!newRecords.length) return;

    // 使用事务确保数据一致性
    await this.repository.manager.transaction(
      async (transactionalEntityManager) => {
        await transactionalEntityManager.save(AttendanceStatistic, newRecords, {
          chunk: 500,
        });
      },
    );
  }
}
